﻿using System.Collections.Generic;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Web;
using System;
using System.Text.RegularExpressions;

namespace ZhuGe.Common
{
    /// <summary>
    /// Responds to a request for CSS files.
    /// 响应请求的文件。{动态生成css引用路径，不用再为路径错误而烦恼}
    /// Use in markup like:  用例：
    /// <link type="text/css" rel="stylesheet" media="all" href="CSSLoader.ashx?~/styles/layout_*.css&~/styles/special.css" />
    ///
    /// </summary><remarks>
    /// Each stylesheet after the '?' marker in the URL will be added in order.
    /// The list is '&' seperated. You can use the kleene star ('*') to select multiple files,
    /// but no multiple directories. You may use the '~/' site root prefix.
    /// <br/>
    /// @import url("file.css"); directives will be found and expanded inline (this
    /// provides a significant performance bonus where many imports are used)
    /// </remarks>
    public class CSSLoader : IHttpHandler
    {
        private HttpContext Context;
        private string currentPath = "";
        HttpResponse Response;
        HttpRequest Request;
        HttpServerUtility Server;
        private StringBuilder Builder;
        private static readonly Regex ImportFinder;

        static CSSLoader()
        {
            ImportFinder = new Regex(@"@import url\((?<file>[^)]*)\);"); // find import directives.
        }

        public void ProcessRequest(HttpContext context)
        {
            #region Setup
            // Link up the names you're used to
            Response = context.Response;
            Request = context.Request;
            Server = context.Server;
            Context = context;
            // prepare the outgoing page:
            Response.ClearContent();
            Response.ClearHeaders();

            // Check for cache-control:
            if (context.Request.IsLocal)
            { // don't cache local requests
                context.Response.AddHeader("Expires", DateTime.Now.ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                context.Response.AddHeader("Cache-Control", "no-cache");
                context.Response.AddHeader("Last-Modified", DateTime.Now.ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
            }
            else
            {
                if (!string.IsNullOrEmpty(Request.Headers["If-Modified-Since"]))
                {
                    string cc = Request.Headers["cache-request-directive"];
                    if (cc != "no-cache" && cc != "no-store")
                    {
                        // Assume no modifications (this responder is for production code!)
                        context.Response.AddHeader("Expires", DateTime.Now.AddYears(5).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                        Response.StatusCode = 304; // not changed
                        Response.End();
                        return;
                    }
                }
                context.Response.AddHeader("Expires", DateTime.Now.AddYears(5).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
                context.Response.AddHeader("Cache-Control", "public");
                context.Response.AddHeader("Last-Modified", DateTime.Now.AddDays(-30).ToString("ddd, dd MMM yyyy hh:mm:ss") + " GMT");
            }

            Response.ContentType = "text/css";
            #endregion

            CompressResponse(); // compress output if possible.

            var urls = new List<string>();
            urls.AddRange(Request.QueryString.ToString().Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries));
            foreach (string url in urls) { AddRequest(url); }

            CompressAndWriteScripts();
            Response.StatusCode = 200;
        }

        private const string GZIP = "gzip";
        private const string DEFLATE = "deflate";

        private void CompressResponse()
        {
            var context = HttpContext.Current;
            try
            {
                if (context.Request.Headers["Accept-encoding"] != null && context.Request.Headers["Accept-encoding"].Contains(DEFLATE)
                || context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"] != null && context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"].Contains(DEFLATE))
                { // prefer Deflate. It doesn't require checksums, and is therefore smaller and faster.
                    Context.Response.Filter = new DeflateStream(Context.Response.Filter, CompressionMode.Compress);
                    Context.Response.AppendHeader("Content-encoding", DEFLATE);
                }
                else if (context.Request.Headers["Accept-encoding"] != null && context.Request.Headers["Accept-encoding"].Contains(GZIP)
              || context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"] != null && context.Request.ServerVariables["HTTP_ACCEPT_ENCODING"].Contains(GZIP))
                {
                    Context.Response.Filter = new GZipStream(Context.Response.Filter, CompressionMode.Compress);
                    Context.Response.AppendHeader("Content-encoding", GZIP);
                }
            }
            catch { }
        }

        /// <summary>
        /// Looks for files with a given pattern, then adds the script if found.
        /// You can use the kleene star ('*') to select multiple files.
        /// </summary>
        /// <param name="pattern"></param>
        private void AddRequest(string pattern)
        {
            if (string.IsNullOrEmpty(pattern)) return;
            if (pattern.StartsWith("version", StringComparison.OrdinalIgnoreCase)) return;
            if (Builder == null) Builder = new StringBuilder();

            string safe_uri = Server.UrlDecode(pattern).Replace("*", "%2a");

            string path = null;
            try
            {
                path = Server.MapPath(safe_uri);
            }
            catch
            {
                // Un-mappable path (usually weird requests)
                if (Builder == null) Builder = new StringBuilder();
                Builder.AppendLine("/* Failed to map path: \"" + (safe_uri ?? "[null]") + "\" */");
                return; // don't bother with this file.
            }

            string root = Path.GetDirectoryName(path);
            string search = Server.UrlDecode(Path.GetFileName(path));

            string[] matches = null;
            try
            {
                matches = Directory.GetFiles(root, search, SearchOption.TopDirectoryOnly);
            }
            catch
            {
                Builder.AppendLine("/* Failed to access files for path: \"" + (safe_uri ?? "[null]") + "\" */");
                return; // don't bother with this file set.
            }

            foreach (string match in matches)
            {
                AddScript(match);
            }
        }

        /// <summary>
        /// Adds a script file to those waiting to be sent
        /// </summary>
        private void AddScript(string path)
        {
            if (Builder == null) Builder = new StringBuilder();
            if (string.IsNullOrEmpty(path)) return;
            if (!File.Exists(path)) return;

            currentPath = Path.GetDirectoryName(path);
            string import = File.ReadAllText(path);

            var importEvaluator = new MatchEvaluator(HonorImport);
            string export = ImportFinder.Replace(import, importEvaluator, int.MaxValue);

            Builder.AppendLine("/* Adding: " + path + " */");

            Builder.AppendLine(export);
            Builder.AppendLine();
        }

        public string HonorImport(Match m)
        {
            string file = m.Groups["file"].Value;
            if (String.IsNullOrEmpty(file)) return m.ToString();

            file = file.Trim(new[] { ' ', '"' });

            string potPath = Path.Combine(currentPath, file);
            if (File.Exists(potPath))
            {
                return File.ReadAllText(potPath);
            }

            potPath = Path.Combine(Path.GetDirectoryName(Context.Request.PhysicalPath), file);
            if (File.Exists(potPath))
            {
                return File.ReadAllText(potPath);
            }

            return m.ToString();
        }

        private void CompressAndWriteScripts()
        {
            if (Builder == null) return;

            string minified = Builder.ToString();
            try
            {
                minified = Minify(minified);
            }
            catch
            {
            }

            Response.Write(minified);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

        #region Minification -- based on PHP minifier by Joe Scylla

        // Need to test that it's worth keeping these compiled
        private static Regex mini_Comments;
        private static Regex mini_Whitespace;
        private static Regex mini_EndBrace;
        private static Regex mini_StartBrace;
        private static Regex mini_Semicolon;
        private static Regex mini_HexValue;
        private static Regex mini_StringValue;
        private static Regex mini_Lists;

        protected string Minify(string src)
        {
            string tmp = src.Replace("\r\n", "\n").Trim();

            RegexOptions opts = RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.ExplicitCapture;
            if (mini_Comments == null) mini_Comments = new Regex(@"\/\*.*?\*\/", opts);
            if (mini_Whitespace == null) mini_Whitespace = new Regex(@"(\t|\s)+", opts);
            if (mini_EndBrace == null) mini_EndBrace = new Regex(@"\}\s+", opts);
            if (mini_Semicolon == null) mini_Semicolon = new Regex(@"\;\s", opts);
            if (mini_StartBrace == null) mini_StartBrace = new Regex(@"\s+\{\s+", opts);
            if (mini_HexValue == null) mini_HexValue = new Regex(@"\:\s+\#", opts);
            if (mini_StringValue == null) mini_StringValue = new Regex(@"\:\s+\'", opts);
            if (mini_Lists == null) mini_Lists = new Regex(@",\s+", opts);

            tmp = mini_Comments.Replace(tmp, "");
            tmp = mini_Whitespace.Replace(tmp, " ");
            tmp = mini_EndBrace.Replace(tmp, "}\n");
            tmp = mini_Semicolon.Replace(tmp, ";");
            tmp = mini_StartBrace.Replace(tmp, "{");
            tmp = mini_HexValue.Replace(tmp, ":#");
            tmp = mini_Lists.Replace(tmp, ",");
            tmp = mini_StringValue.Replace(tmp, ":'");
            tmp = mini_Whitespace.Replace(tmp, " ");

            return tmp;
        }

        #endregion
    }
}
